import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.io.*;
import java.util.Random;

/**
 * Created by berkbozkurt on 07/03/15.
 */
/*
LogicController: Singleton


 */
public class GameController {

    private Scoreboard scoreboard;
    private LifeView lifeView;
    private Random powerupGenerator;
    private PowerUpFactory powerupFactory;
    private boolean gameRunning = true;
    private long lastFpsTime;
    private boolean paused = false;
    private int fps = 60;
    private long updateLength = 0;
    private boolean running = true;
    private static GameController instance;
    private GameInteraction gameInteraction;
    private GamePanel gamePanel;
    private MenuPanel menuPanel;
    private int level;
    private boolean cheatKeyPressed = false;
    // Keys
    private boolean upPressed;
    private boolean downPressed;
    private boolean leftPressed;
    private boolean rightPressed;
    private int life;
    private double elapsedTimeSinceBallsLost;
    private boolean hasAnyBall;
    private float cheatTimer;
    private float powerUpTimer;
    private boolean givingPowerUp;


    private StateInformation currentState;

    // Singleton class so shouldn't be instantiated with new ...
    private GameController() {
        // Initial state is MenuState
        // Just call configureState() after updating the state
        MenuState state = new MenuState();
        currentState = new StateInformation(state);
        state.updateState(currentState);
        configureState();
    }

    public void configureState() {
        if (currentState.getState().toString().equalsIgnoreCase("MenuState")) {
            menuPanel = new MenuPanel();
            menuPanel.setBounds(0, 100, 600, 400);
            Application.getInstance().getContentPane().add(menuPanel);
            Application.getInstance().getContentPane().setBackground(Color.black);
            Application.getInstance().getContentPane().repaint();
            Application.getInstance().getContentPane().validate();
        } else if (currentState.getState().toString().equalsIgnoreCase("PlayingState")) {

            // Remove all previous components from view
            Application.getInstance().getContentPane().removeAll();
            // Construct InteractionPanel(Ball, Bricks, Player, Surprise objects are in this panel)
            life = 3;
            level = 1;

            powerUpTimer = 0;
            cheatTimer = 100;
            powerupGenerator = new Random();
            powerupFactory = new PowerUpFactory();
            gameInteraction = new GameInteraction();
            gameRunning = true;
            gameInteraction.lastPowerUp = "";
            gameInteraction.setLocation(0, 0);
            gameInteraction.setOpaque(false);
            gameInteraction.setBorder(null);
            gameInteraction.setLayout(null);
            gameInteraction.constructLevel(level);


            scoreboard = new Scoreboard();
            scoreboard.setLocation(250, 480);

            addKeyBindings(gameInteraction);

            // Life View
            lifeView = new LifeView();
            lifeView.setLocation(5, 15);
            for (Ball ball : gameInteraction.getBalls()) {
                ball.yVelocity = -2f;
            }
            Application.getInstance().getContentPane().add(gameInteraction);
            Application.getInstance().getContentPane().add(lifeView);
            Application.getInstance().getContentPane().add(scoreboard);

            gameRunning = true;
            paused = false;

            // Repaint so changes appear
            Application.getInstance().getContentPane().repaint();
            Application.getInstance().getContentPane().validate();

            // Call Game Loop
            runGameLoop();
        } else if (currentState.getState().toString().equalsIgnoreCase("WinState")) {
            paused = true;
            System.out.println("Winstate");
            // Check high score
            String highScoreString = "Score: ";

            if (scoreboard.getScore() < Application.getInstance().highScore) {
                highScoreString = "New High Score: ";

            }

            // Remove all previous components from view
            gameRunning = false;
            Application.getInstance().getContentPane().removeAll();

            System.out.println("WinState");
            // All levels finished
            JLabel winLabel = new JLabel();
            winLabel.setSize(500, 180);
            winLabel.setText("<html>Congratulations<br>" + highScoreString + scoreboard.getScore());
            winLabel.setLocation(30, 30);
            winLabel.setForeground(Color.white);
            JButton startGameButton = new JButton();
            startGameButton.setText("New Game");

            try {
                InputStream myStream = new BufferedInputStream(new FileInputStream("resources/game.ttf"));
                Font font = Font.createFont(Font.TRUETYPE_FONT, myStream);
                Font biggerFont = font.deriveFont(Font.PLAIN, 36f);
                winLabel.setFont(biggerFont);
                startGameButton.setFont(biggerFont);
            } catch (IOException | FontFormatException ex) {
                ex.printStackTrace();
            }
            startGameButton.setSize(new Dimension(300, 60));
            startGameButton.setForeground(Color.black);
            startGameButton.setLocation(winLabel.getX() + 80, 300);
            startGameButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    PlayingState playingState = new PlayingState();
                    playingState.updateState(getCurrentState());
                    configureState();
                }
            });
            Application.getInstance().getContentPane().add(winLabel);
            Application.getInstance().getContentPane().add(startGameButton);
            Application.getInstance().getContentPane().repaint();
            Application.getInstance().getContentPane().validate();

            try {
                Font font = Font.createFont(Font.TRUETYPE_FONT, new File("resources/game.ttf"));
                Font biggerFont = font.deriveFont(Font.PLAIN, 36f);
                winLabel.setFont(biggerFont);
                startGameButton.setFont(biggerFont);
            } catch (IOException | FontFormatException ex) {
                ex.printStackTrace();
            }


        } else if (currentState.getState().toString().equalsIgnoreCase("LoseState")) {
            paused = true;
            // Check high score
            String highScoreString = "Score: ";

            if (scoreboard.getScore() < Application.getInstance().highScore) {
                highScoreString = "New High Score: ";

            }

            System.out.println("Lose State");

            gameRunning = false;
            Application.getInstance().getContentPane().removeAll();
            gameInteraction.removeAll();

            // All levels finished
            JLabel winLabel = new JLabel();
            winLabel.setSize(500, 180);
            winLabel.setText("<html>Game Over<br>" + highScoreString + scoreboard.getScore());
            winLabel.setLocation(30, 30);
            winLabel.setForeground(Color.white);
            JButton startGameButton = new JButton();

            try {
                InputStream myStream = new BufferedInputStream(new FileInputStream("resources/game.ttf"));
                Font font = Font.createFont(Font.TRUETYPE_FONT, myStream);
                Font biggerFont = font.deriveFont(Font.PLAIN, 20f);
                winLabel.setFont(biggerFont);
                startGameButton.setFont(biggerFont);
            } catch (IOException | FontFormatException ex) {
                ex.printStackTrace();
            }
            startGameButton.setSize(new Dimension(300, 60));
            startGameButton.setText("New Game");
            startGameButton.setForeground(Color.black);
            startGameButton.setLocation(winLabel.getX() + 80, 300);
            startGameButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    PlayingState playingState = new PlayingState();
                    playingState.updateState(getCurrentState());
                    configureState();
                }
            });
            Application.getInstance().getContentPane().add(winLabel);
            Application.getInstance().getContentPane().add(startGameButton);
            Application.getInstance().getContentPane().repaint();
            Application.getInstance().getContentPane().validate();

            try {
                Font font = Font.createFont(Font.TRUETYPE_FONT, new File("resources/game.ttf"));
                Font biggerFont = font.deriveFont(Font.PLAIN, 24f);
                winLabel.setFont(biggerFont);
            } catch (IOException | FontFormatException ex) {
                ex.printStackTrace();
            }

        }else

        {
            // Throw State exception
        }

    }

    // Use this method to access LogicController
    public static GameController getInstance() {
        if (instance == null) {
            instance = new GameController();
        }
        return instance;
    }

    //Starts a new thread and runs the game loop in it.
    public void runGameLoop() {
        Thread loop = new Thread() {
            public void run() {
                gameLoop();
            }
        };
        loop.start();
    }


    public void gameLoop() {
        long lastLoopTime = System.nanoTime();
        final int TARGET_FPS = 120;
        final long OPTIMAL_TIME = 1000000000 / TARGET_FPS;

        // keep looping round til the game ends
        while (gameRunning) {
            // work out how long its been since the last update, this
            // will be used to calculate how far the entities should
            // move this loop

            long now = System.nanoTime();
            updateLength = now - lastLoopTime;
            lastLoopTime = now;
            double delta = updateLength / ((double) OPTIMAL_TIME);

            // update the frame counter
            lastFpsTime += updateLength;
            fps++;

            // update our FPS counter if a second has passed since
            // we last recorded
            if (lastFpsTime >= 1000000000) {
                System.out.println("(FPS: " + fps + ")");
                gameInteraction.fps = fps;
                lastFpsTime = 0;
                fps = 0;

            }

            // update the game logic
            doGameUpdates(delta);

            // draw everything
            render();

            // we want each frame to take 10 milliseconds, to do this
            // we've recorded when we started the frame. We add 10 milliseconds
            // to this and then factor in the current time to give
            // us our final value to wait for
            // remember this is in ms, whereas our lastLoopTime etc. vars are in ns.
            try {
                //
                Thread.sleep((Math.abs(lastLoopTime - System.nanoTime() + OPTIMAL_TIME) / 1000000));
                //Thread.sleep(Math.abs((lastLoopTime - System.nanoTime() + OPTIMAL_TIME) / 1000000));
            } catch (InterruptedException e) {
                //e.printStackTrace();
            }
        }
    }

    private void render() {

        gameInteraction.repaint();
        scoreboard.repaint();
        lifeView.repaint();
    }
    // Game play Logic

    private void doGameUpdates(double delta) {


        if (!paused) {
            Paddle paddle = gameInteraction.getPaddle();

            // Paddle and powerUps Collision
            for (int i = 0; i < gameInteraction.getPowerUps().size(); i++) {

                PowerUp powerUp = gameInteraction.getPowerUps().get(i);

                if (powerUp.intersects(paddle)) {

                    if (powerUp.getTargetClassName().equals(Paddle.class.getName())) {
                        if (powerUp.getType().equals("Longer Paddle"))
                            gameInteraction.createPaddle("default");
                        else if (powerUp.getType().equals("Shorter Paddle"))
                            gameInteraction.createPaddle("shorter");

                    } else if (powerUp.getTargetClassName().equals(Ball.class.getName())) {

                        if (powerUp.getType().equals("Triple Ball")) {
                            Ball newBall = null;
                            Ball newBall2 = null;
                            for (Ball ball : gameInteraction.getBalls()) {

                                newBall = new Ball(gameInteraction.getSize());
                                newBall.xVelocity = (float) (Math.random() - 0.5) * 5.0f;
                                newBall.yVelocity = -2;
                                newBall.x = ball.x;
                                newBall.y = ball.y;

                                newBall2 = new Ball(gameInteraction.getSize());
                                newBall2.xVelocity = (float) (Math.random() - 0.5) * 5.0f;
                                newBall2.yVelocity = -2;
                                newBall2.x = ball.x;
                                newBall2.y = ball.y;
                            }
                            if (newBall != null) {
                                gameInteraction.getBalls().add(newBall);
                                gameInteraction.getBalls().add(newBall2);
                            }
                        } else {
                            for (Ball ball : gameInteraction.getBalls()) {
                                ball.applyEffect(powerUp.getType());

                            }
                        }
                    }

                    gameInteraction.destroyPowerUp(i);
                }
            }

            // Balls and Paddle Collision
            for (Ball ball : gameInteraction.getBalls()) {
                if (ball.intersects(paddle) && ball.yVelocity > 0) {
                    ball.yVelocity = ball.yVelocity * -1;
                    ball.xVelocity = paddle.xVelocity * (float) Math.random();
                }
            }

            // Ensure doesn't create powerUp more than once
            if (givingPowerUp) {
                powerUpTimer += delta;
                if (powerUpTimer > 200) {
                    givingPowerUp = false;
                    powerUpTimer = 0;
                }
            }

            // Balls and Brick Collision
            for (int i = 0; i < gameInteraction.getBricks().size(); i++) {
                for (Ball ball : gameInteraction.getBalls()) {
                    Brick brick = gameInteraction.getBricks().get(i);
                    brick.update();
                    Rectangle brickRect = (Rectangle) brick;

                    if (ball.intersects(brickRect)) {
                        // Handle score
                        if (brick instanceof SoftBrick) {
                            scoreboard.addScore(80);
                        } else if (brick instanceof MediumBrick) {
                            scoreboard.addScore(120);
                        } else {
                            scoreboard.addScore(150);
                        }
                        // Generate PowerUp by %30 chance
                        if (powerUpTimer <= 0 && !givingPowerUp) {
                            givingPowerUp = true;
                            int randomNum = powerupGenerator.nextInt(10);

                            if (randomNum > 0) {
                                PowerUp powerup = powerupFactory.createPowerUp(
                                        (int) (((Rectangle) brick).getFrame().getX() + ((Rectangle) brick).getWidth() / 2),
                                        (int) (((Rectangle) brick).getFrame().getY() + ((Rectangle) brick).getHeight() / 2));
                                gameInteraction.getPowerUps().add(powerup);
                                gameInteraction.lastPowerUp = powerup.getType();
                            }
                        }
                        ball.yVelocity *= -1;
                        // Add bricks that has 0 life to destroyable ArrayList
                        // TODO:
                        gameInteraction.destroyBrick(i);
                    }
                }
            }

            // Update powerUps speed according to delta time
            for (PowerUp powerUp : gameInteraction.getPowerUps()) {
                powerUp.update(delta);
            }

            // Update ball and player according to delta time
            for (int i = 0; i < gameInteraction.getBalls().size(); i++) {
                Ball ball = gameInteraction.getBalls().get(i);
                ball.update(delta);

                // Limit balls max speed
                if (ball.xVelocity >= 8) {
                    ball.xVelocity = 8 * (float) delta;
                }
                if (ball.yVelocity >= 4) {

                    ball.yVelocity = 8 * (float) delta;
                }

                // If ball is in screen and moving
                if (ball.y - 40 < gameInteraction.getHeight() && !(ball.xVelocity == 0 && ball.yVelocity == 0)) {
                    hasAnyBall = true;
                }
                // Ball is not in screen
                if (ball.y - 40 > gameInteraction.getHeight()) {
                    System.out.println("Destroy ball");
                    gameInteraction.destroyBall(i);
                }

            }
            // Increment timer (Time to start the ball after going down)

            if (gameInteraction.getBalls().size() == 0 && !hasAnyBall) {

                Ball ball = new Ball(gameInteraction.getSize());
                ball.xVelocity = 0;
                ball.yVelocity = 0;
                ball.x = gameInteraction.getWidth() / 2;
                ball.y = gameInteraction.getHeight() / 2;
                gameInteraction.getBalls().add(ball);
            }

            if (!hasAnyBall) {
                elapsedTimeSinceBallsLost += delta;
            }

            //if (!hasAnyBall && )
            paddle.update(delta);


            // If timer exceeds 200 start the ball with random velocity
            if (elapsedTimeSinceBallsLost > 200) {
                System.out.println("ball start");
                for (Ball ball : gameInteraction.getBalls()) {

                    ball.xVelocity = ((float) Math.random() - 0.5f) * 4.0f;
                    ball.yVelocity = 2.0f;
                    hasAnyBall = true;
                    elapsedTimeSinceBallsLost = 0;
                }
            }
            // Cheat
            if (cheatKeyPressed && hasAnyBall) {
                cheatTimer -= delta;
                if (cheatTimer < 0) {
                    PowerUp powerUp = powerupFactory.createTripleBall(
                            (int) gameInteraction.getBalls().get(0).getX(),
                            (int) gameInteraction.getBalls().get(0).getY());

                    gameInteraction.getPowerUps().add(powerUp);
                    cheatTimer = 100;
                }
            }

            if (gameInteraction.getBricks().size() == 0) {

                System.out.println("Next level");
                ++level;
                // next level
                // Remove Balls
                for (int i = 0; i < gameInteraction.getBalls().size(); i++) {
                    gameInteraction.destroyBall(i);
                }

                for (int i = 0; i < gameInteraction.getPowerUps().size(); i++) {
                    gameInteraction.destroyPowerUp(i);
                }
                // Construct level
                gameInteraction.getBalls().clear();
                if (level < 4) {
                    gameInteraction.constructLevel(level);
                } else {
                    WinState state = new WinState();
                    currentState = new StateInformation(state);
                    configureState();
                }
                // Add new ball
                Ball ball = new Ball(gameInteraction.getSize());
                ball.xVelocity = 0;
                ball.yVelocity = 0;
                ball.x = gameInteraction.getWidth() / 2;
                ball.y = gameInteraction.getHeight() / 2;
                gameInteraction.getBalls().add(ball);

                hasAnyBall = false;
                // Add new ball by setting hasAnyBall to false
            }
            // Delete all game objects that added to destroyable ArrayList
            gameInteraction.removeDestroyedBricks();
            gameInteraction.removeDestroyedBalls();
            gameInteraction.removeDestroyedPowerUps();

            if (gameInteraction.getBalls().size() == 0) {
                gameInteraction.createPaddle("default");
                hasAnyBall = false;
                life--;
                lifeView.decreaseLife();
                if (life == 0) {
                    LoseState state = new LoseState();
                    currentState = new StateInformation(state);
                    configureState();
                }
            }

        }
    }

    public static void addKeyBindings(JComponent jc) {
        jc.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false), "Up pressed");
        jc.getActionMap().put("Up pressed", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                GameController.getInstance().setUpPressed(true);
            }
        });
        jc.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "Up released");
        jc.getActionMap().put("Up released", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                GameController.getInstance().setUpPressed(false);
            }
        });

        jc.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false), "Down pressed");
        jc.getActionMap().put("Down pressed", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                GameController.getInstance().setDownPressed(true);
            }
        });

        jc.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "Down released");
        jc.getActionMap().put("Down released", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                GameController.getInstance().setDownPressed(false);
            }
        });

        jc.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false), "Left pressed");
        jc.getActionMap().put("Left pressed", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                GameController.getInstance().setLeftPressed(true);
            }
        });
        jc.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, true), "Left released");
        jc.getActionMap().put("Left released", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                GameController.getInstance().setLeftPressed(false);
            }
        });
        jc.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false), "Right pressed");
        jc.getActionMap().put("Right pressed", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                GameController.getInstance().setRightPressed(true);
            }
        });
        jc.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, true), "Right released");
        jc.getActionMap().put("Right released", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                GameController.getInstance().setRightPressed(false);
            }
        });
        jc.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_Q, 0, false), "Q pressed");
        jc.getActionMap().put("Q pressed", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                GameController.getInstance().setCheatKeyPressed(true);
            }
        });
        jc.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_Q, 0, true), "Q released");
        jc.getActionMap().put("Q released", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                GameController.getInstance().setCheatKeyPressed(false);
            }
        });
        jc.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, false), "ESC pressed");
        jc.getActionMap().put("ESC pressed", new AbstractAction() {
            @Override
            public void actionPerformed(ActionEvent ae) {
                GameController.getInstance().pauseGame();
            }
        });

    }

    public StateInformation getCurrentState() {
        return currentState;
    }

    public void setCurrentState(StateInformation currentState) {
        this.currentState = currentState;
    }

    // Accessor methods
    public boolean isUpPressed() {
        return upPressed;
    }

    public void setUpPressed(boolean upPressed) {
        this.upPressed = upPressed;
    }

    public boolean isDownPressed() {
        return downPressed;
    }

    public void setDownPressed(boolean downPressed) {
        this.downPressed = downPressed;
    }

    public boolean isLeftPressed() {
        return leftPressed;
    }

    public void setLeftPressed(boolean leftPressed) {
        this.leftPressed = leftPressed;
    }

    public boolean isRightPressed() {
        return rightPressed;
    }

    public void setRightPressed(boolean rightPressed) {
        this.rightPressed = rightPressed;
    }

    public void setCheatKeyPressed(boolean b) {
        this.cheatKeyPressed = b;
    }

    public void pauseGame() {
        if (paused) paused = false;
        else paused = true;
    }
}
